/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.search;

import static org.apache.solr.search.RankQParserPlugin.EXPONENT;
import static org.apache.solr.search.RankQParserPlugin.FIELD;
import static org.apache.solr.search.RankQParserPlugin.FUNCTION;
import static org.apache.solr.search.RankQParserPlugin.NAME;
import static org.apache.solr.search.RankQParserPlugin.PIVOT;
import static org.apache.solr.search.RankQParserPlugin.SCALING_FACTOR;
import static org.apache.solr.search.RankQParserPlugin.WEIGHT;

import java.io.IOException;
import org.apache.lucene.search.Query;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.RankField;
import org.apache.solr.search.RankQParserPlugin.RankQParser;
import org.hamcrest.CoreMatchers;
import org.junit.BeforeClass;

public class RankQParserPluginTest extends SolrTestCaseJ4 {

  @BeforeClass
  public static void beforeClass() throws Exception {
    initCore("solrconfig-minimal.xml", "schema-rank-fields.xml");
  }

  public void testParamCompatibility() {
    assertEquals("RankQParserPlugin.NAME changed in an incompatible way", "rank", NAME);
    assertEquals("RankQParserPlugin.FIELD changed in an incompatible way", "f", FIELD);
    assertEquals("RankQParserPlugin.FUNCTION changed in an incompatible way", "function", FUNCTION);
    assertEquals("RankQParserPlugin.PIVOT changed in an incompatible way", "pivot", PIVOT);
    assertEquals(
        "RankQParserPlugin.SCALING_FACTOR changed in an incompatible way",
        "scalingFactor",
        SCALING_FACTOR);
    assertEquals("RankQParserPlugin.WEIGHT changed in an incompatible way", "weight", WEIGHT);
    assertEquals("RankQParserPlugin.EXPONENT changed in an incompatible way", "exponent", EXPONENT);
  }

  public void testCreateParser() throws IOException {
    RankQParserPlugin rankQPPlugin = new RankQParserPlugin();
    QParser parser = rankQPPlugin.createParser("", SolrParams.of(), SolrParams.of(), req());
    assertNotNull(parser);
    assertTrue(parser instanceof RankQParser);
  }

  public void testSyntaxErrors() {
    assertSyntaxError(
        "No Field",
        "Field can't be empty",
        () -> getRankQParser(new ModifiableSolrParams(), req()).parse());
    assertSyntaxError(
        "Field empty",
        "Field can't be empty",
        () -> getRankQParser(params(FIELD, ""), req()).parse());
    assertSyntaxError(
        "Field doesn't exist",
        "Field \"foo\" not found",
        () -> getRankQParser(params(FIELD, "foo"), req()).parse());
    assertSyntaxError(
        "ID is not a feature field",
        "Field \"id\" is not a RankField",
        () -> getRankQParser(params(FIELD, "id"), req()).parse());
  }

  public void testBadLinearParameters() {
    assertSyntaxError(
        "Expecting bad weight",
        "weight must be in",
        () ->
            getRankQParser(
                    params(
                        FIELD, "rank_1",
                        FUNCTION, "linear",
                        WEIGHT, "0"),
                    req())
                .parse());
  }

  public void testBadLogParameters() {
    assertSyntaxError(
        "Expecting bad weight",
        "weight must be in",
        () ->
            getRankQParser(
                    params(
                        FIELD, "rank_1",
                        FUNCTION, "log",
                        WEIGHT, "0"),
                    req())
                .parse());
    assertSyntaxError(
        "Expecting bad scaling factor",
        "scalingFactor must be",
        () ->
            getRankQParser(
                    params(
                        FIELD, "rank_1",
                        FUNCTION, "log",
                        SCALING_FACTOR, "0"),
                    req())
                .parse());
  }

  public void testBadSaturationParameters() {
    assertSyntaxError(
        "Expecting a pivot value",
        "A pivot value",
        () ->
            getRankQParser(
                    params(
                        FIELD, "rank_1",
                        FUNCTION, "satu",
                        WEIGHT, "2"),
                    req())
                .parse());
    assertSyntaxError(
        "Expecting bad weight",
        "weight must be in",
        () ->
            getRankQParser(
                    params(
                        FIELD, "rank_1",
                        FUNCTION, "satu",
                        PIVOT, "1",
                        WEIGHT, "-1"),
                    req())
                .parse());
  }

  public void testBadSigmoidParameters() {
    assertSyntaxError(
        "Expecting missing pivot",
        "A pivot value",
        () ->
            getRankQParser(
                    params(
                        FIELD, "rank_1",
                        FUNCTION, "sigm",
                        EXPONENT, "1"),
                    req())
                .parse());
    assertSyntaxError(
        "Expecting missing exponent",
        "An exponent value",
        () ->
            getRankQParser(
                    params(
                        FIELD, "rank_1",
                        FUNCTION, "sigm",
                        PIVOT, "1"),
                    req())
                .parse());
    assertSyntaxError(
        "Expecting bad weight",
        "weight must be in",
        () ->
            getRankQParser(
                    params(
                        FIELD, "rank_1",
                        FUNCTION, "sigm",
                        PIVOT, "1",
                        EXPONENT, "1",
                        WEIGHT, "-1"),
                    req())
                .parse());
    assertSyntaxError(
        "Expecting bad pivot",
        "pivot must be",
        () ->
            getRankQParser(
                    params(
                        FIELD, "rank_1",
                        FUNCTION, "sigm",
                        PIVOT, "0",
                        EXPONENT, "1"),
                    req())
                .parse());
    assertSyntaxError(
        "Expecting bad exponent",
        "exp must be",
        () ->
            getRankQParser(
                    params(
                        FIELD, "rank_1",
                        FUNCTION, "sigm",
                        PIVOT, "1",
                        EXPONENT, "0"),
                    req())
                .parse());
  }

  public void testUnknownFunction() {
    assertSyntaxError(
        "Expecting bad function",
        "Unknown function in rank query: \"foo\"",
        () ->
            getRankQParser(
                    params(
                        FIELD, "rank_1",
                        FUNCTION, "foo"),
                    req())
                .parse());
  }

  public void testParseLinear() throws IOException, SyntaxError {
    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedLinearToString(), 1),
        params(
            FIELD, "rank_1",
            FUNCTION, "linear",
            WEIGHT, "1"));

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedLinearToString(), 1),
        params(
            FIELD, "rank_1",
            FUNCTION, "linear",
            WEIGHT, "1"));

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedLinearToString(), 2.5f),
        params(
            FIELD, "rank_1",
            FUNCTION, "linear",
            WEIGHT, "2.5"));

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedLinearToString(), 2.5f),
        params(
            FIELD, "rank_1",
            FUNCTION, "Linear", // use different case
            WEIGHT, "2.5"));
  }

  public void testParseLog() throws IOException, SyntaxError {
    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedLogToString(1), 1),
        params(
            FIELD, "rank_1",
            FUNCTION, "log",
            SCALING_FACTOR, "1",
            WEIGHT, "1"));

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedLogToString(2.5f), 1),
        params(
            FIELD, "rank_1",
            FUNCTION, "log",
            SCALING_FACTOR, "2.5",
            WEIGHT, "1"));

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedLogToString(1), 2.5f),
        params(
            FIELD, "rank_1",
            FUNCTION, "log",
            SCALING_FACTOR, "1",
            WEIGHT, "2.5"));

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedLogToString(1), 2.5f),
        params(
            FIELD, "rank_1",
            FUNCTION, "Log", // use different case
            SCALING_FACTOR, "1",
            WEIGHT, "2.5"));
  }

  public void testParseSigm() throws IOException, SyntaxError {
    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedSigmoidToString(1.5f, 2f), 1),
        params(
            FIELD, "rank_1",
            FUNCTION, "sigm",
            PIVOT, "1.5",
            EXPONENT, "2",
            WEIGHT, "1"));

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedSigmoidToString(1.5f, 2f), 2),
        params(
            FIELD, "rank_1",
            FUNCTION, "sigm",
            PIVOT, "1.5",
            EXPONENT, "2",
            WEIGHT, "2"));
  }

  public void testParseSatu() throws IOException, SyntaxError {

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedSaturationToString(1.5f), 1),
        params(
            FIELD, "rank_1",
            FUNCTION, "satu",
            PIVOT, "1.5",
            WEIGHT, "1"));

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedSaturationToString(1.5f), 2),
        params(
            FIELD, "rank_1",
            FUNCTION, "satu",
            PIVOT, "1.5",
            WEIGHT, "2"));

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedSaturationToString(null), 1),
        params(
            FIELD, "rank_1",
            FUNCTION, "satu"));

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedSaturationToString(null), 1),
        params(
            FIELD, "rank_1",
            FUNCTION, "satu",
            WEIGHT, "1"));

    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedSaturationToString(1.5f), 1),
        params(
            FIELD, "rank_1",
            FUNCTION, "satu",
            PIVOT, "1.5"));
  }

  public void testParseDefault() throws IOException, SyntaxError {
    assertValidRankQuery(
        expectedFeatureQueryToString("rank_1", expectedSaturationToString(null), 1),
        params(FIELD, "rank_1"));
  }

  private void assertValidRankQuery(String expectedToString, SolrParams localParams)
      throws IOException, SyntaxError {
    QParser parser = getRankQParser(localParams, req());
    Query q = parser.parse();
    assertNotNull(q);
    assertThat(q.toString(), CoreMatchers.equalTo(expectedToString));
  }

  private String expectedFeatureQueryToString(String fieldName, String function, float boost) {
    String featureQueryStr =
        "FeatureQuery(field="
            + RankField.INTERNAL_RANK_FIELD_NAME
            + ", feature="
            + fieldName
            + ", function="
            + function
            + ")";
    if (boost == 1f) {
      return featureQueryStr;
    }
    return "(" + featureQueryStr + ")^" + boost;
  }

  private String expectedLinearToString() {
    return "LinearFunction";
  }

  private String expectedLogToString(float scalingFactor) {
    return "LogFunction(scalingFactor=" + scalingFactor + ")";
  }

  private String expectedSigmoidToString(float pivot, float exp) {
    return "SigmoidFunction(pivot=" + pivot + ", a=" + exp + ")";
  }

  private String expectedSaturationToString(Float pivot) {
    return "SaturationFunction(pivot=" + pivot + ")";
  }

  private void assertSyntaxError(
      String assertionMsg, String expectedExceptionMsg, ThrowingRunnable runnable) {
    SyntaxError se = expectThrows(SyntaxError.class, assertionMsg, runnable);
    assertThat(se.getMessage(), CoreMatchers.containsString(expectedExceptionMsg));
  }

  private RankQParser getRankQParser(SolrParams localParams, SolrQueryRequest req) {
    RankQParserPlugin rankQPPlugin = new RankQParserPlugin();
    return (RankQParser) rankQPPlugin.createParser("", localParams, req.getParams(), req);
  }
}
